# h1=. vid.py

# p=. *Visual Interface Dialect* 
# _jfuller 2011-07-05_

# Building GUIs in wx or wxPython is an exercise in misery. Surely
# someone has figured out an /easier/ way? Well, actually, they
# have. Just not for Python. This is a port of the REBOL Visual
# Interface Dialect to wxPython.

# See existing
# "documentation":http://www.rebol.com/docs/view-guide.html for
# details on layout features of this library, with these exceptions:

# * No STYLE definitions. Yet. 
# * TABS can only be specified as a single number. 
# * Only text, h1, box, image, button, toggle and choice styles available. 

# h2. Front Matter

# h3. Imports

import sys; sys.path.append("..")
import string, re, pprint, wx
import pyPEG as PEG
import paths, gui


class face(object):
    def __init__(my, parent, *args):
        my.parent = parent

        a = (("label", "unlabeled"),
             ("ink",   wx.Colour("red")),
             ("paper", wx.Colour("green")),
             ("size",  (64, 64)),
             ("at",    (-1, -1)),
             ("font",  wx.Font(10, wx.FONTFAMILY_SWISS)))

        for key, default in a:
            if type(args[0]) == type(default):
                setattr(my, key, args[0])
                args = args[1:]
            else: setattr(my, key, default)


    def __setattr__(my, key, value):
        my.__dict__[name] = value

        if   "parent" == key: my.SetParent(value)
        elif "label"  == key: my.SetLabel(my.label)
        elif "ink"    == key: my.SetForgroundColour(my.ink)
        elif "paper"  == key: my.SetBackgroundColour(my.paper)
        elif "size"   == key: my.SetSize(my.size)
        elif "at"     == key: my.SetPos(my.at)
        elif "font"   == key: my.SetFont(my.font)


    def get(my):
        return my.label


    def set(my, value):
        my.label = value
        my.Refresh()


class text(wx.StaticText, face):
    def __init__(my, parent, *args):
        apply(wx.StaticText.__init__, my, (parent, -1))
        apply(face, my, parent)
    

# h3. Built-in Widget Styles

# This is the default setup for the widgets available in the VID.

styles = {
    "text": (
        gui.text,
        ("label", "vstring", "unlabled"),
        ("ForegroundColour", "vtuple", (0, 0, 0)),
        ("BackgroundColour", "vtuple", (200, 200, 200)),
        ("size", "vpair", (256, 16)),
        ("pos", "vpair", None),
        ),
    "h1": (
        gui.readable,
        ("label", "vstring", "unlabled"),
        ("ForegroundColour", "vtuple", (0, 0, 0)),
        ("BackgroundColour", "vtuple", (200, 200, 200)),
        ("size", "vpair", (256, 16)),
        ("pos", "vpair", None),
        ),
    "score": (
        gui.score,
        ("label", "vstring", "unlabled"),
        ("ForegroundColour", "vtuple", (0, 0, 0)),
        ("BackgroundColour", "vtuple", (200, 200, 200)),
        ("size", "vpair", (256, 16)),
        ("pos", "vpair", None),
        ),
    "area": (
        gui.area,
        ("value", "vstring", ""),
        ("BackgroundColour", "vtuple", (200, 200, 200)),
        ("ForegroundColour", "vtuple", (0, 0, 0)),
        ("size", "vpair", (256, 16)),
        ("pos", "vpair", None),
        ),
    "box": (
        gui.panel,
        ("BackgroundColour", "vtuple", (60, 108, 174)),
        ("ForegroundColour", "vtuple", (255, 255, 255)),
        ("size", "vpair", (100, 100)),
        ("pos", "vpair", None),
        ),
    "image": (
        gui.image,
        ("file", "vfile", None),
        ("size", "vpair", (100, 100)),
        ("pos", "vpair", None),
        ),
    "button": (
        gui.btn,
        ("label", "vstring", "unlabled"),
        ("BackgroundColour", "vtuple", (60, 108, 174)),
        ("ForegroundColour", "vtuple", (255, 255, 255)),
        ("size", "vpair", (80, 24)),
        ("pos", "vpair", None),
        ("action", "vblock", None),
        ),
    "toggle": (
        gui.tgl,
        ("label", "vstring", "unlabled"),
        ("BackgroundColour", "vtuple", (60, 108, 174)),
        ("altcolour",        "vtuple", (60, 108, 174)),
        ("ForegroundColour", "vtuple", (255, 255, 255)),
        ("size", "vpair", (80, 24)),
        ("pos", "vpair", None),
        ("action", "vblock", None),
        ),
    "choice": (
        gui.facet,
        ("name", "vstring", "choice"),
        ("ForegroundColour", "vtuple", (0, 0, 0)),
        ("BackgroundColour", "vtuple", (200, 200, 200)),
        ("size", "vpair", (80, 24)),
        ("pos", "vpair", None),
        ("action", "vblock", None),
        ),
    }


styles = {
    "text":   gui.text,
    "h1":     gui.readable,
    "area":   gui.area,
    "box":    gui.panel,
    "image":  gui.image,
    "button": gui.btn,
    "toggle": gui.tgl,
    "choice": gui.facet,
    }

# h3. VID spec parse ruleset

# This parser ruleset defines the Visual Interface Dialect. Note that
# at this point, new base styles have to be added to the style table
# above as well as here. See "pyPEG
# documentation":http://fdik.org/pyPEG/ for more information.

def vnumber(): return re.compile("-?\d+")
def vpair():   return (vnumber, "x", vnumber)
def vtuple():  return (vnumber, ".", vnumber, ".", vnumber)
def vname():   return re.compile("[a-zA-Z][a-zA-Z0-9_]*")
def vstring(): return '"', re.compile('[^"]*'), '"'
def vfile():   return "%", re.compile("[a-zA-Z][-a-zA-Z0-9_/.]+")
def vblock():  return "[", re.compile('[^]]*'), "]"

def vacross(): return "across"
def vdown():   return "down"
def vreturn(): return "return"
def vtab():    return "tab"

def vat():     return "at", vpair
def vsize():   return "size", vpair
def vbg():     return "backcolor", vtuple
def vorigin(): return "origin", vpair
def vspace():  return "space", 0, vpair, 0, vnumber
def vtabs():   return "tabs", vnumber
def vpad():    return "pad", 0, vpair, 0, vnumber
def vbox():    return "box", 0, vtuple, 0, vtuple, 0, vpair, 0, vpair
def vimage():  return "image", vfile, 0, vpair, 0, vpair
def vbutton(): return "button", -1, [vstring, vtuple, vpair, vblock]
def vtoggle(): return "toggle", -1, [vstring, vtuple, vpair, vblock]
def vchoice(): return "choice", -1, [vstring, vtuple, vpair, vblock]
def vtext():   return "text", -1, [vstring, vtuple, vpair]
def vh1():     return "h1", -1, [vstring, vtuple, vpair]
def vscore():  return "score", -1, [vstring, vtuple, vpair]
def varea():   return "area", -1, [vstring, vtuple, vpair]

def vexp():    return [
    vacross, vdown, vreturn, vtab,
    vat, vsize, vbg, vorigin, vspace, vtabs,
    vpad, vbox, vimage, vtext, vh1, vscore, varea, vtoggle, vbutton, vchoice]
def vassign(): return vname, ":", vexp
def vspec():   return -2, [vassign, vexp]

# h3. Convenience Functions

# h4. Predicates

def intp(data): return isinstance(data, int)

def stringp(data): # I hate unicode...
    return isinstance(data, basestring)

def tuplep(data): return isinstance(data, tuple)

def listp(data): return isinstance(data, list)

def hashp(data):
    return isinstance(data, dict)

# These functions collapse values out of the abstract syntax tree. For
# convenience! Almost like for science, but more convenient!

def renumber(ast):
    if "vnumber" != ast[0]: return ast # Wrong type.
    return int(ast[-1])

def repair(ast):
    if ast[0] not in ["vnumber", "vpair"]: return ast # Wrong type.
    if "vnumber" == ast[0]: n = renumber(ast); return (n, n) # Number to tuple.
    return (renumber(ast[1][0]), renumber(ast[1][1]))

def retuple(ast):
    if "vtuple" != ast[0]: return ast # Wrong type.
    return (renumber(ast[1][0]), renumber(ast[1][1]), renumber(ast[1][2]))

def revalue(ast):
    "( ast -- value) Break value out of ast."

    if   "vnumber" == ast[0]: return renumber(ast)
    elif "vpair"   == ast[0]: return repair(ast)
    elif "vtuple"  == ast[0]: return retuple(ast)
    elif "vstring" == ast[0]: return ast[1][0]
    elif "vfile"   == ast[0]: return paths.path(ast[1][0])
    return ast[1]

# h2. Pane

class pane(object):
    def __init__(my):
        my.styles = styles
        my.reset()


    def reset(my):
        my.backcolor = (200, 200, 200)
        my.extents = (0, 0)
        my.direction = (0, 1)
        my.origin = (20, 20)
        my.spacing = (8, 8)
        my.cursor = my.origin[:]

        my.current = None

        my.spec = []
        my.names = {}
        my.actions = {}


    def link(my, *actions):
        "( my action... --) Attach functions to the action dispatch table."

        for action in actions: my.actions[action.func_name] = action


    def close(my):
        "( my --) Close current style definition."

        if not my.current: return

        x = my.spec; face = my.spec[-1]

        used = face[1].keys()
        for parm in my.current[1:]:
            if parm[0] in used: continue
            if not parm[2]: continue
            x[-1][-1][parm[0]] = parm[2]
        my.current = None

        if "action" in face[1].keys() and None != face[1]["action"]:
            a = face[1]["action"][0]
            face[1]["action"] = lambda e: my.actions[a](e)
            # if a in my.actions: face[1]["action"] = my.actions[a]
            # else: del(face[1]["action"]) # Remove uninitialized actions.

        my.cursor = [
            my.cursor[0] + ((my.spacing[0] + x[-1][-1]["size"][0]) * my.direction[0]),
            my.cursor[1] + ((my.spacing[0] + x[-1][-1]["size"][1]) * my.direction[1])]
        my.extents = (
            max(my.extents[0], x[-1][-1]["pos"][0] + x[-1][-1]["size"][0]),
            max(my.extents[1], x[-1][-1]["pos"][1] + x[-1][-1]["size"][1]))


    def layout(my, spec):
        my.reset()

        for vexp in spec:
            token, args = vexp[0], vexp[1:]
            
            if   "across" == token: my.direction = (1, 0)
            elif "down"   == token: my.direction = (0, 1)
            elif "return" == token:
                if (1, 0) == my.direction:
                    my.cursor = (my.origin[0], my.extents[1] + my.spacing[1])
                else:
                    my.cursor = (my.extents[0] + my.spacing[0], my.origin[1])
            elif "pad"    == token:
                o = (0, 0)
                if len args:
                    if tuplep(args[0]): o = args[0]
                    elif intp(args[0]):
                        o = (my.direction[0] * args[0],
                             my.direction[1] * args[0])
                my.cursor = (my.cursor[0] + o[0], my.cursor[1] + o[1])
            elif "tab"    == token: pass
            elif "at"     == token: my.cursor = args[0]
            elif "size"   == token: my.extents = args[0]
            elif "bg"     == token: my.backcolor = args[0]
            elif "origin" == token: my.origin = args[0]
            elif "space"  == token: my.spacing = args[0]
            elif "tabs"   == token: my.tabs = args[0]
            elif token in my.styles:
                # XXX Put new face building here. josh 2011-11-23
                
                my.current = my.styles[token[1:]]
                face = [my.current[0], {"pos": my.cursor}]
                my.spec.append(face)

                for parm in my.current[1:]:
                    for i in range(len(args)):
                        arg = args[i]
                        kind = arg[0]; value = revalue(arg)
                        if parm[1] == kind:
                            face[-1][parm[0]] = value
                            del(args[i])
                            break

                my.close()
                        
        my.extents = (my.extents[0] + my.origin[0],
                      my.extents[1] + my.origin[1])
        return my

# This @layout()@ method is provided to make setting up a gui into a
# single function call. Note that the pane that it returns is just a
# data structure until you @view@ it.

def layout(spec, *actions):
    p = pane().layout(spec)
    apply(p.link, actions)
    
    return p

# Normally, the main program would manage the app. However, I'm trying
# to keep that out of the user's face, so I make it a module variable
# instead.

app = None

def start():
    global app
    app = wx.App(redirect=False)
    return app


class view_frame(wx.Frame):
    def __init__(my, *args, **kwargs):
        apply(wx.Frame.__init__, (my,) + args, kwargs)

        i = paths.path("program.ico")
        if i.exists():
            try: my.SetIcon(wx.Icon(i(), wx.BITMAP_TYPE_ICO))
            except: pass # Oh well!

        
    def __getitem__(my, key):
        kids = my.GetChildren()

        if gui.stringp(key):
            for kid in kids:
                if key == kid.GetName(): return kid
        else: return kids[key]

        raise KeyError


def view(pane, title=None, size=None, offset=None, new=False):
    if not app: start() # Init app if needed.
    if not title: title = "Untitled"
    if not size: size = pane.extents

    frame = view_frame(None, wx.ID_ANY, title)
    frame.SetClientSize(size)
    frame.SetBackgroundColour(pane.backcolor)

    # pprint.pprint(pane.spec) # Debug, prints out pane spec.
    for i in pane.spec:
        apply(i[0], (frame, wx.ID_ANY), i[1])

    if offset: frame.SetPosition(offset)
    frame.Show()

    if not new: app.MainLoop()
    
    return frame


def do_events():
    app.MainLoop()


# Running this module standalone will display a (very) simple example
# of how to use the VID. First I define whatever event handlers the
# pane will need, then I create a layout by describing the GUI in the
# Visual Interface Dialect and attaching references to the event
# handlers, then I @view@ the layout. View activates the @wxApp@ if
# needed and starts the event loop unless otherwise instructed.

if "__main__" == __name__:
    def message_close(e): e.GetEventObject().GetParent().Close()

    def message_show():
        view(layout("""\
backcolor 200.200.255
text "There is no dispute possible."  20.20.20  200.200.255
across
button "Agreed!" [message_close]
pad 100x2
button "I refute!" [message_close]
""", message_close))


    def main_inspect(e):
        import wx.lib.inspection
        wx.lib.inspection.InspectionTool().Show()

    def main_uniform(e): message_show()

    def main_jump(e):
        import os
        os.startfile({
            "Buso Renkin": "https://en.wikipedia.org/wiki/Buso_Renkin",
            "Slashdot"   : "http://www.slashdot.org",
            "Reddit"     : "http://www.reddit.com"}[main["sites"].GetValue()])

#     main = view(layout("""\
# text "VID Demo"
# image %tokiko.jpg 400x293 
# button "Tokiko seems to prefer the Newton Apple Uniform" 400x20 [main_uniform]

# across
# choice "sites" 100x50
# button "go!" 50x50 [main_jump]

# pad 153x2
# button "inspect!" [main_inspect]
# """, main_inspect, main_uniform, main_jump), new=True)

#     main["sites"].SetItems(("Buso Renkin", "Slashdot", "Reddit"))

#     do_events()

    main = view(layout(
        ("text", "VID Demo"),
        ("image", "tokiko.jpg", (400, 293)),
        ("button", "Tokiko seems to prefer the Newton Apple Uniform",
               (400, 20), main_uniform),

        "across",
        ("choice", "sites", (100x50),
               ("Buso Renkin", "Slashdot", "Reddit")),
        ("button", "go!", (50, 50), main_jump),

        ("pad", 153),
        ("button", "inspect!", main_inspect)))
